Node.js 基于JavaScript编写应用,JavaScript是我的主要开发语言。CoffeeScript是编译为JavaScript的编程语言。为什么我们要用CoffeeScript来编写一段可重用的代码——模块呢?CoffeeScript是一个非常高阶的语言,将JavaScript、Ruby和Python中我最爱的部分结合在了一起。在本教程中,我将展示如何使用CoffeeScript为Node.js创建一个可复用的开源模块。最近我在创建一个播放列表分析模块时get了这个新技能。重点在于如何将一个快速的开发变成一个结构良好的Node.js模块。

步骤如下:

  1. 将创意放入git仓库。
  2. 添加目录结构。
  3. 从测试中分离库函数。
  4. 添加构建脚本。
  5. 创建node模块。
  6. 添加LICENSE 和 README。
  7. 发布。

首先,我们需要一个创意。它不用是多么革命性的创意。它只需做一件事,并且将它做好。这是UNIX饱受争议的哲学的第一条准则,在Node.js社区激起了共鸣。当我开发的时候,我从单一文件开始,进行一些探索。然后我渐进地改良代码直到我做出了可复用的东西。这样,我们可以复用它,别人也可以复用它,别人也可以从代码中得到启发,世界会因此更美好。

本教程中,我将展示如何为nanomsg创建一个绑定。nanomsg是ZeroMQ的创造者 Martin Sústrik最新开发的一个可伸锁性协议库。我以前曾经玩过ZeroMQ,感觉它非常棒。当我看到ZeroMQ的作者做出了一个基于C的新库的时候,我非常激动。因为我很喜欢他的博客《为什么我应该用C而不是C++编写 ZeroMQ》

为了快速地上手,我们首先确保node的版本够新。我喜欢使用nvm,以及最新的稳定minor版node(版本格式为major.minor.patch,稳定版的minor数字是偶数,所以v0.11.0是非稳定版)。

node -v
-> v0.10.17

接下来我需要下载我打算动态链接的库:

curl -O http://download.nanomsg.org/nanomsg-0.1-alpha.zip && \
unzip nanomsg-0.1-alpha.zip && \
cd nanomsg-0.1-alpha && \
mkdir build && \
cd build && \
../configure && \
make && \
make install

我们将使用node的FFI模块,以便和动态链接库交互。对于编写绑定而言,这比使用原生扩展要容易,而且V8的API最近的修改给原生扩展造成了一些麻烦

npm install ffi

我们将使用CoffeeScript编写代码:

npm install -g coffee-script

C++绑定样例的基础上我们创建一个main.coffee

ffi = require 'ffi'
assert = require 'assert'

AF_SP = 1
NN_PAIR = 16

nanomsg = ffi.Library 'libnanomsg',
  nn_socket: [ 'int', [ 'int', 'int' ]]
  nn_bind: [ 'int', [ 'int', 'string' ]]
  nn_connect: [ 'int', ['int', 'string' ]]
  nn_send: [ 'int', ['int', 'pointer', 'int', 'int']]
  nn_recv: [ 'int', ['int', 'pointer', 'int', 'int']]
  nn_errno: [ 'int', []]

# test
s1 = nanomsg.nn_socket AF_SP, NN_PAIR
assert s1 >= 0, 's1: ' + nanomsg.nn_errno()

ret = nanomsg.nn_bind s1, 'inproc://a'
assert ret > 0, 'bind'

s2 = nanomsg.nn_socket AF_SP, NN_PAIR
assert s2 >= 0, 's2: ' + nanomsg.nn_errno()

ret = nanomsg.nn_connect s2, 'inproc://a'
assert ret > 0, 'connect'

msg = new Buffer 'hello'
ret = nanomsg.nn_send s2, msg, msg.length, 0
assert ret > 0, 'send'

recv = new Buffer msg.length
ret = nanomsg.nn_recv s1, recv, recv.length, 0
assert ret > 0, 'recv'

console.log recv.toString()
assert msg.toString() is recv.toString(), 'received message did not match sent'
coffee main.coffee
-> hello

这个快速编写的例子显示我们已经能做到一些事情了。目前我们的目录结构是这样的:

tree -L 2
.
├── main.coffee
└── node_modules
    └── ffi

2 directories, 1 file

将创意变为git仓库

接着我们使用git创建一个仓库,开始保存我们的工作。更早提交,更多提交

让我们加入一个.gitignore文件,这样不需要提交的文件就不会被加入到git仓库。node_modules文件夹是不必要提交的,因为当安装node模块的时候,它的依赖会被递归地安装,所以没有必要将它们提交到源代码管理系统。因为我使用vim,所以还需要排除vim的交换文件:

node_modules/
*.swp

好了,让我们创建git仓库吧。

git init && \
git add . && \
git commit -am "initial commit"

在github上创建一个未初始化的仓库,然后推送:

git remote add origin git@github.com:nickdesaulniers/node-nanomsg.git && \
git push -u origin master

现在我们的目录结构如下:

tree -L 2 -a
.
├── .gitignore
├── main.coffee
└── node_modules
    └── ffi

2 directories, 2 files

添加目录结构

既然我的代码已经处于git之下,让我们开始添加一些目录结构。我们需要创建src/lib/test/目录。src/放置我们的CoffeeScript,lib/放置编译的JavaScript文件,我们的测试代码会在test/

mkdir src lib test

从测试中分离库函数

现在我们把main.coffee移动到src/,并把它的一个副本移动到test/。我们将从测试逻辑中分离出库函数。

cp main.coffee test/test.coffee && \
git add test/test.coffee && \
git mv main.coffee src/nanomsg.coffee

git status告诉我们:

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# renamed:    main.coffee -> src/nanomsg.coffee
# new file:   test/test.coffee
#

让我们修改下 src/main.coffee

ffi = require 'ffi'

exports = module.exports = ffi.Library 'libnanomsg',
  nn_socket: [ 'int', [ 'int', 'int' ]]
  nn_bind: [ 'int', [ 'int', 'string' ]]
  nn_connect: [ 'int', ['int', 'string' ]]
  nn_send: [ 'int', ['int', 'pointer', 'int', 'int']]
  nn_recv: [ 'int', ['int', 'pointer', 'int', 'int']]
  nn_errno: [ 'int', []]

exports.AF_SP = 1
exports.NN_PAIR = 16

并且修改测试:

assert = require 'assert'
nanomsg = require '../lib/nanomsg.js'

{ AF_SP, NN_PAIR } = nanomsg

s1 = nanomsg.nn_socket AF_SP, NN_PAIR
assert s1 >= 0, 's1: ' + nanomsg.nn_errno()

ret = nanomsg.nn_bind s1, 'inproc://a'
assert ret > 0, 'bind'

s2 = nanomsg.nn_socket AF_SP, NN_PAIR
assert s2 >= 0, 's2: ' + nanomsg.nn_errno()

ret = nanomsg.nn_connect s2, 'inproc://a'
assert ret > 0, 'connect'

msg = new Buffer 'hello'
ret = nanomsg.nn_send s2, msg, msg.length, 0
assert ret > 0, 'send'

recv = new Buffer msg.length
ret = nanomsg.nn_recv s1, recv, recv.length, 0
assert ret > 0, 'recv'

assert msg.toString() is recv.toString(), 'received message did not match sent'

注意到了我们在test中包含了尚不存在的lib/下的JavaScript文件?如果我们尝试运行coffee test/test.coffee,它会崩溃。让我们先编译一下。coffee -o lib -c src/nanomsg.coffee

编译完成之后,使用coffee test/test.coffee运行我们的测试。

现在让我们提交一下吧。注意不要把lib/加入版本控制,我下面会解释为什么。

tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── lib
│   └── nanomsg.js
├── node_modules
│   └── ffi
├── src
│   └── nanomsg.coffee
└── test
    └── test.coffee

5 directories, 4 files

就目前而言,如果我们添加了特性并打算运行测试,我们需要执行:

coffee -o lib -c src/nanomsg.coffee && coffee test/test.coffee

虽然这个命令很简单,也不难理解,但是任何贡献代码给你的项目的人需要知道这个命令才能运行测试。让我们使用Grunt,JavaScript任务自动化工具来自动化我们的构建和测试过程。

添加一个构建脚本

npm install -g grunt-cli && \
npm install grunt-contrib-coffee

用CoffeeScript创建一个简单的Gruntfile:

module.exports = (grunt) ->
  grunt.initConfig
    coffee:
      compile:
        files:
          'lib/nanomsg.js': ['src/*.coffee']
  grunt.loadNpmTasks 'grunt-contrib-coffee'
  grunt.registerTask 'default', ['coffee']

运行grunt将创建我们的库,让我们提交一下

但是grunt不会运行我们的测试。我们的测试输出也不好看。让我们改变这一点:

npm install -g mocha && \
npm install chai grunt-mocha-test

编辑test/test.coffee

assert = require 'assert'
should = require('chai').should()
nanomsg = require '../lib/nanomsg.js'

describe 'nanomsg', ->
  it 'should at least work', ->
    { AF_SP, NN_PAIR } = nanomsg

    s1 = nanomsg.nn_socket AF_SP, NN_PAIR
    s1.should.be.at.least 0

    ret = nanomsg.nn_bind s1, 'inproc://a'
    ret.should.be.above 0

    s2 = nanomsg.nn_socket AF_SP, NN_PAIR
    s2.should.be.at.least 0

    ret = nanomsg.nn_connect s2, 'inproc://a'
    ret.should.be.above 0

    msg = new Buffer 'hello'
    ret = nanomsg.nn_send s2, msg, msg.length, 0
    ret.should.be.above 0

    recv = new Buffer msg.length
    ret = nanomsg.nn_recv s1, recv, recv.length, 0
    ret.should.be.above 0

    msg.toString().should.equal recv.toString()

然后编辑我们的gruntfile,加入测试步骤:

module.exports = (grunt) ->
  grunt.initConfig
    coffee:
      compile:
        files:
          'lib/nanomsg.js': ['src/*.coffee']
    mochaTest:
      options:
        reporter: 'nyan'
      src: ['test/test.coffee']

  grunt.loadNpmTasks 'grunt-contrib-coffee'
  grunt.loadNpmTasks 'grunt-mocha-test'

  grunt.registerTask 'default', ['coffee', 'mochaTest']

现在,当我们运行grunt的时候,将构建我们的程序,然后运行测试,然后我们可以看到开心死了的彩虹猫彩虹猫mocha测试报告差不多是人类心智所能达到的最高成就。

grunt
Running "coffee:compile" (coffee) task
File lib/nanomsg.js created.

Running "mochaTest:src" (mochaTest) task
 1   -__,------,
 0   -__|  /\_/\
 0   -_~|_( ^ .^)
     -_ ""  ""

  1 passing (5 ms)


Done, without errors.

又到了提交的时候了。

tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── Gruntfile.coffee
├── lib
│   └── nanomsg.js
├── node_modules
│   ├── ffi
│   ├── grunt
│   └── grunt-contrib-coffee
├── src
│   └── nanomsg.coffee
└── test
    └── test.coffee

7 directories, 5 files

创建node模块

现在我们的设计已经比较模块化了,内置了构建和测试的逻辑,让我们使这个模块容易再分发吧。首先,我们讨论忽略的文件。创建一个.npmignore文件,它将指定哪些文件不会被包含在下载中。Node包管理程序,npm,默认会忽略一组文件文件

Gruntfile.coffee
src/
test/

默认忽略了src/目录,在我们的.gitignore中则忽略了lib/

node_modules/
lib/
*.swp

为何如此?老实说,在严格意义上而言,忽略这两个目录不是必需的。但是我认为这很有用。当有人获取代码的时候,他不需要编译的结果,毕竟他们可以进行修改,而这需要重新编译。添加lib/nanomsg.js将增加下载的文件(当然它的大小相对而言无关紧要)。同理,当有人下载模块的时候,他多半只想要编译好的文件,而不是源代码、构建脚本或测试。如果我希望让浏览器可以访问编译好的JavaScript,我可能不会在.gitignore中包含lib/,这样就可以通过github的raw URL引用它了。当然,这些只是一般情况下的经验,并不总是正确的。为了弥补不把整个代码放进模块的缺憾,我们将在manifest中添加一个指向仓库的链接。不过在此之前,让我们先提交一下!

现在该是创建manifest文件的时候,其中包含了我们的应用的基本信息。预先使用npm search <packagename>看看打算使用的包名是否可用是个很好的注意。由于我们已经安装了所有依赖,让我们运行npm init吧。

This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sane defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (nanomsg)
version: (0.0.0)
description: nanomsg bindings
entry point: (index.js) lib/nanomsg.js
test command: grunt
git repository: (git://github.com/nickdesaulniers/node-nanomsg.git)
keywords: nanomsg
author: Nick Desaulniers
license: (BSD-2-Clause) Beerware
About to write to /Users/Nicholas/code/c/nanomsg/package.json:

{
  "name": "nanomsg",
  "version": "0.0.0",
  "description": "nanomsg bindings",
  "main": "lib/nanomsg.js",
  "directories": {
    "test": "test"
  },
  "dependencies": {
    "chai": "~1.7.2",
    "ffi": "~1.2.5",
    "grunt": "~0.4.1",
    "grunt-mocha-test": "~0.6.3",
    "grunt-contrib-coffee": "~0.7.0"
  },
  "devDependencies": {},
  "scripts": {
    "test": "grunt"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/nickdesaulniers/node-nanomsg.git"
  },
  "keywords": [
    "nanomsg"
  ],
  "author": "Nick Desaulniers",
  "license": "Beerware",
  "bugs": {
    "url": "https://github.com/nickdesaulniers/node-nanomsg/issues"
  }
}


Is this ok? (yes)

这将为npm创建一个package.json manifest。

现在,除了使用grunt之外,我们也可以通过npm test来运行我们的测试了。在发布模块之前,先提交一下

tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── .npmignore
├── Gruntfile.coffee
├── lib
│   └── nanomsg.js
├── node_modules
│   ├── .bin
│   ├── chai
│   ├── ffi
│   ├── grunt
│   ├── grunt-contrib-coffee
│   └── grunt-mocha-test
├── package.json
├── src
│   └── nanomsg.coffee
└── test
    └── test.coffee

10 directories, 7 files

添加 LICENSE 和 README

现在我们差不多已经完成了。但是开发者如何知道该如何复用这些代码呢?不管我有多么喜欢直接查看源代码,npm会抱怨我们的模块没有一个readme文件。而且有readme的话,github仓库会比较好看。

# Node-NanoMSG
Node.js binding for [nanomsg](http://nanomsg.org/index.html).

## Usage

`npm install nanomsg`

```javascript
var nanomsg = require('nanomsg');
var assert = require('assert');
var AF_SP = nanomsg.AF_SP;
var NN_PAIR = nanomsg.NN_PAIR;
var msg = new Buffer('hello');
var recv = new Buffer(msg.length);
var s1, s2, ret;

s1 = nanomsg.nn_socket(AF_SP, NN_PAIR);
assert(s1 >= 0, 's1: ' + nanomsg.errno());

ret = nanomsg.nn_bind(s1, 'inproc://a');
assert(ret > 0, 'bind');

s2 = nanomsg.nn_socket(AF_SP, NN_PAIR);
assert(s2 >= 0, 's2: ' + nanomsg.errno());

ret = nanomsg.nn_connect(s2, 'inproc://a');
assert(ret > 0, 'connect');

ret = nanomsg.nn_send(s2, msg, msg.length, 0);
assert(ret > 0, 'send');

ret = nanomsg.recv(s1, recv, recv.length, 0);
assert(ret > 0, 'recv');

assert(msg.toString() === recv.toString(), "didn't receive sent message");
console.log(recv.toString());

发布之前,我们需要创建一个许可文件,因为我们将公开我们的代码,没有明确许可的公开代码仍然处于版权保护之下,不可复用

/*
 * ----------------------------------------------------------------------------
 * "THE BEER-WARE LICENSE" (Revision 42):
 * <nick@mozilla.com> wrote this file. As long as you retain this notice you
 * can do whatever you want with this stuff. If we meet some day, and you think
 * this stuff is worth it, you can buy me a beer in return. Nick Desaulniers
 * ----------------------------------------------------------------------------
 */
 ```

如果你希望正经一点,你可以使用MIT或BSD风格的许可,如果你不在乎你的仓库会被如何使用。如果你在乎,可以使用GPL风格的许可。[TLDRLegal](http://www.tldrlegal.com/)对常见的许可协议都有简要的说明。

```sh
tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── .npmignore
├── Gruntfile.coffee
├── LICENSE
├── README.md
├── lib
│   └── nanomsg.js
├── node_modules
│   ├── .bin
│   ├── chai
│   ├── ffi
│   ├── grunt
│   ├── grunt-contrib-coffee
│   └── grunt-mocha-test
├── package.json
├── src
│   └── nanomsg.coffee
└── test
    └── test.coffee

10 directories, 9 files

发布

npm publish

npm http PUT https://registry.npmjs.org/nanomsg
npm http 201 https://registry.npmjs.org/nanomsg
npm http GET https://registry.npmjs.org/nanomsg
npm http 200 https://registry.npmjs.org/nanomsg
npm http PUT https://registry.npmjs.org/nanomsg/-/nanomsg-0.0.0.tgz/-rev/1-20f1ec5ca2eed51e840feff22479bb5d
npm http 201 https://registry.npmjs.org/nanomsg/-/nanomsg-0.0.0.tgz/-rev/1-20f1ec5ca2eed51e840feff22479bb5d
npm http PUT https://registry.npmjs.org/nanomsg/0.0.0/-tag/latest
npm http 201 https://registry.npmjs.org/nanomsg/0.0.0/-tag/latest
+ nanomsg@0.0.0

最后,我喜欢在别的地方创建一个新目录,然后根据readme中的步骤跑一遍,以确保包确实可以复用。这很有用,因为在readme中我不小心遗漏了 errno 和 recv 前的nn_前缀!

更新readme中的例子之后,让我们修改版本号并重新发布。使用npm version查看当前版本,然后使用npm version patch来修改。在此之前你需要提交readme的改动。最后,别忘了再次运行npm publish

我们最终的目录结构看起来是这样的:

tree -L 2 -C -a -I '.git'
.
├── .gitignore
├── .npmignore
├── Gruntfile.coffee
├── LICENSE
├── README.md
├── lib
│   └── nanomsg.js
├── node_modules
│   ├── .bin
│   ├── chai
│   ├── ffi
│   ├── grunt
│   ├── grunt-contrib-coffee
│   └── grunt-mocha-test
├── package.json
├── src
│   └── nanomsg.coffee
└── test
    └── test.coffee

10 directories, 9 files

最后,我会联系下Martin Sústrik,让他知道nanomsg有一个新绑定了。

这个绑定远远不够完整,测试覆盖率可以更高,API非常像C,可以使用一些OO语法糖的,但是我们已经有了一个良好的起点,可以进一步改进了。如果你有意帮忙,请派生 https://github.com/nickdesaulniers/node-nanomsg.git

你对node模块的构建步骤、测试、目录结构有什么想法?这个教程显然不会是一个权威指南。我期待你们的评论。


原文 Making Great Node.js Modules With CoffeeScript
翻译 SegmentFault


weakish
24.6k 声望844 粉丝

a vigorously lazy deadbeat with matured immaturity